﻿using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using Emgu.CV;
using Emgu.CV.Structure;
using Domotica.Gazing.Calibration;
using System.Windows.Threading;

namespace Domotica.Gazing
{
    public class Gaze : IDisposable
    {
        #region Constructor

        public Gaze(bool wpf)
        {
            _wpf = wpf;
            DrawFace = DrawFace.All;

            LoadConfig();

            Initialize();

            if (_wpf)
            {
                _GazeCursorWindow = new GazeCursorWindow();
                _GazeCursorWindow.Show();
            }
            else
            {
                _CameraCursor = new GazeCursorForm();
                _CameraCursor.Show();
            }

            _Clicker = new GazeClicker(this, _Config);

            _Calibrator = new Calibrator(_Config, _wpf);


            StartCapture();
        }

        private void LoadConfig()
        {
            _Config = Config.Load();
            if (_Config == null)
            {
                _Config = new Config();
            }
        }

        #endregion

        #region properties
        private bool _wpf;

        private Image<Bgr, byte> _CurrentImage;

        private EyeState _LeftEyeStatus = EyeState.Unknown;
        private Point _Position;

        private EyeState _RightEyeStatus = EyeState.Unknown;
        public DrawFace DrawFace { get; set; }

        private PointF _TrackingPoint = Point.Empty;
        public PointF TrackingPoint
        {
            get
            {
                return _TrackingPoint;
            }
        }

        public Image<Bgr, byte> CurrentImage
        {
            get { return _CurrentImage; }
        }

        public EyeState LeftEyeStatus
        {
            get
            {
                if (_LeftEyeInit)
                {
                    return EyeState.Unknown;
                }
                return _LeftEyeStatus;
            }
        }

        public EyeState RightEyeStatus
        {
            get
            {
                if (_RightEyeInit)
                {
                    return EyeState.Unknown;
                }
                return _RightEyeStatus;
            }
        }

        public Point Position
        {
            get { return _Position; }
        }

        #endregion

        #region Events

        public bool CaptureInProgress
        {
            get { return _CaptureInProgress; }
        }

        public event GazeEventHandler FaceMove;

        private void OnFaceMove(GazeEventArgs e)
        {
            if (_wpf)
            {
                _GazeCursorWindow.Left = e.Position.X;
                _GazeCursorWindow.Top = e.Position.Y;
            }
            else
            {
                _CameraCursor.SetLocation(e.Position);
            }
            _Calibrator.OnFaceMove(e);
            if (FaceMove != null)
            {
                FaceMove(this, e);
            }
        }

        public event ImageProcessedEventHandler ImageProcessed;

        private void OnImageProcessed(ImageProcessedEventArgs e)
        {
            if (ImageProcessed != null)
            {
                ImageProcessed(this, e);
            }
        }

        public event GazeClickEventHandler GazeClicked;

        internal void OnGazeClick(GazeClickEventArgs e)
        {
            _Calibrator.OnGazeClick(e);
            if (GazeClicked != null)
            {
                GazeClicked(this, e);
            }
        }

        public event GazeClickElapsedEventHandler GazeClickedElapsed;

        internal void OnGazeClickElapsedChanged(GazeClickElapsedEventArgs e)
        {
            _Calibrator.OnGazeClickedElapsed(e);
            if (GazeClickedElapsed != null)
            {
                GazeClickedElapsed(this, e);
            }
        }

        #endregion

        #region Fields

        private readonly Calibrator _Calibrator;
        private readonly GazeCursorForm _CameraCursor;
        private readonly GazeCursorWindow _GazeCursorWindow;
        private readonly GazeClicker _Clicker;
        private Camera _Camera;
        private Capture _Capture;
        private bool _CaptureInProgress;
        private Config _Config;
        private FeatureTracker _FaceFeatureTracker;
        private bool _FaceInit = true;
        private FeatureTracker _LeftEyeFeatureTracker;
        private bool _LeftEyeInit = true;
        private Face _RecognizedFace;
        private FeatureTracker _RightEyeFeatureTracker;
        private bool _RightEyeInit = true;
        private bool UseCapture = true;
        private DispatcherTimer _Timer;

        #endregion

        #region Methods

        public void Dispose()
        {
            if (_Capture != null)
                _Capture.Dispose();
            if (_Camera != null)
                _Camera.Dispose();
        }

        private bool CheckDrawFace(DrawFace drawFace)
        {
            return (DrawFace & drawFace) != DrawFace.None;
        }
        private void ProcessImage(object sender, EventArgs arg)
        {
            ProcessImage();
        }
        private void ProcessImage()
        {
            Image<Bgr, byte> image = ProcessCapture();

            if (image != null)
            {
                _CurrentImage = image;
                Image<Gray, Byte> grayFrame = image.Convert<Gray, Byte>();
                //normalizes brightness and increases contrast of the image
                grayFrame._EqualizeHist();

                ProcessInitialize(image, grayFrame);

                if (!_FaceInit)
                {
                    if (!_Calibrator.Calibrated)
                    {
                        _Calibrator.Quick();
                    }
                }
                ProcessFace(image, grayFrame);

                ProcessRightEye(image, grayFrame);

                ProcessLeftEye(image, grayFrame);
            }
            OnImageProcessed(new ImageProcessedEventArgs(this));
        }

        private Image<Bgr, byte> ProcessCapture()
        {
            Image<Bgr, Byte> image = null;
            if (UseCapture)
            {
                if (_Capture != null)
                {
                    image = _Capture.QueryFrame();
                }
            }
            else
            {
                if (_Camera != null)
                {
                    Bitmap bm = _Camera.GetCurrentImage();
                    if (bm != null)
                    {
                        image = new Image<Bgr, byte>(bm);
                    }
                }
            }
            return image;
        }

        private void ProcessLeftEye(Image<Bgr, byte> image, Image<Gray, byte> grayFrame)
        {
            if (_LeftEyeFeatureTracker != null && !_LeftEyeInit)
            {
                if (_LeftEyeFeatureTracker.Track(grayFrame))
                {
                    if (CheckDrawFace(DrawFace.EyeFeatures))
                    {
                        _LeftEyeFeatureTracker.DrawFeatures(image);
                    }
                    if (CheckDrawFace(DrawFace.EyeTracking))
                    {
                        _LeftEyeFeatureTracker.DrawTrackingPoint(image);
                    }
                    _LeftEyeStatus = _LeftEyeFeatureTracker.CheckEyeStatus();
                    if (_LeftEyeStatus == EyeState.ReOpen)
                    {
                        _LeftEyeInit = true;
                        Thread.Sleep(100);
                    }
                }
                else
                {
                    _LeftEyeInit = true;
                }
            }
        }

        private void ProcessRightEye(Image<Bgr, byte> image, Image<Gray, byte> grayFrame)
        {
            if (_RightEyeFeatureTracker != null && !_RightEyeInit)
            {
                if (_RightEyeFeatureTracker.Track(grayFrame))
                {
                    if (CheckDrawFace(DrawFace.EyeFeatures))
                    {
                        _RightEyeFeatureTracker.DrawFeatures(image);
                    }
                    if (CheckDrawFace(DrawFace.EyeTracking))
                    {
                        _RightEyeFeatureTracker.DrawTrackingPoint(image);
                    }
                    _RightEyeStatus = _RightEyeFeatureTracker.CheckEyeStatus();
                    if (_RightEyeStatus == EyeState.Closed)
                    {
                    }
                    else if (RightEyeStatus == EyeState.ReOpen)
                    {
                        _RightEyeInit = true;
                        Thread.Sleep(100);
                    }
                }
                else
                {
                    _RightEyeInit = true;
                }
            }
        }

        private void ProcessFace(Image<Bgr, byte> image, Image<Gray, byte> grayFrame)
        {
            if (_FaceFeatureTracker != null)
            {
                if (_FaceFeatureTracker.Track(grayFrame))
                {
                    if (CheckDrawFace(DrawFace.FaceFeatures))
                    {
                        _FaceFeatureTracker.DrawFeatures(image);
                    }
                    if (CheckDrawFace(DrawFace.FaceTracking))
                    {
                        _FaceFeatureTracker.DrawTrackingPoint(image);
                    }
                    _Clicker.CheckClicker();
                    if (CalculateFacePosition())
                    {
                        OnFaceMove(new GazeEventArgs(this));
                    }
                }
                else
                {
                    _FaceInit = true;
                    _RightEyeInit = true;
                    _LeftEyeInit = true;
                }
            }
        }

        private void ProcessInitialize(Image<Bgr, byte> image, Image<Gray, byte> grayFrame)
        {
            if (_FaceInit || _RightEyeInit || _LeftEyeInit)
            {
                var recognizedImage = new FaceTracker(grayFrame);
                if (_FaceInit)
                {
                    if (CheckDrawFace(DrawFace.FaceBox))
                    {
                        recognizedImage.DrawFaces(image);
                    }
                }
                if (CheckDrawFace(DrawFace.EyeBox))
                {
                    recognizedImage.DrawEyes(image);
                }
                if (recognizedImage.HasOneFace())
                {
                    _RecognizedFace = recognizedImage.Faces[0];

                    if (_FaceInit)
                    {
                        _FaceFeatureTracker = new FeatureTracker(grayFrame, _RecognizedFace.CalculateRectangle());
                    }
                    if (_RightEyeInit)
                    {
                        _RightEyeFeatureTracker = new FeatureTracker(grayFrame, _RecognizedFace.RightEye.Rectangle);
                        _RightEyeInit = !_RightEyeFeatureTracker.Valid;
                    }
                    if (_LeftEyeInit)
                    {
                        _LeftEyeFeatureTracker = new FeatureTracker(grayFrame, _RecognizedFace.LeftEye.Rectangle);
                        _LeftEyeInit = !_LeftEyeFeatureTracker.Valid;
                    }
                    _FaceInit = false;
                }
            }
        }

        private bool LimitTrackpointDelta(PointF cur, PointF last)
        {
            if (last != Point.Empty)
                if (Math.Sqrt(Math.Pow((cur.X - last.X), 2.0)
                              + Math.Pow((cur.Y - last.Y), 2.0)) > 35.0)
                {
                    return false;
                }
            return true;
        }

        private bool CalculateFacePosition()
        {
            if (LimitTrackpointDelta(_FaceFeatureTracker.TrackingPoint,
                                     _TrackingPoint))
            {
                PointF cur = Image2Screen(_FaceFeatureTracker.TrackingPoint);

                float damping = _Config.Smoothing;
                _Position = new Point((int) ((cur.X*damping) + (Position.X*(1f - damping))),
                                      (int) ((cur.Y*damping) + (Position.Y*(1f - damping))));
                return true;
            }
            _TrackingPoint = _FaceFeatureTracker.TrackingPoint;
            return false;
        }

        private PointF Image2Screen(PointF p)
        {
            float x = p.X;
            float y = p.Y;

            float imageWidth = _Config.ImageSize.Width/2f;
            float imageHeight = _Config.ImageSize.Height/2f;
            float middleX = _Config.ScreenMiddle.X;
            float middleY = _Config.ScreenMiddle.Y;
            float xg = ((float) _Config.ScreenSize.Width/2)/imageWidth;
            float yg = ((float) _Config.ScreenSize.Height/2)/imageHeight;

            x = (_Config.HorizontalGain*(imageWidth - x))*xg;
            if (_Config.ReverseHorizon)
            {
                x *= -1f;
            }
            x += middleX;

            y = ((-1f*_Config.VerticalGain)*(imageHeight - y))*yg;
            y += middleY;

            if (_Config.Calibrated)
            {
                x += _Config.ScreenOffset.Width;
                y += _Config.ScreenOffset.Height;
            }
            return new PointF(x, y);
        }

        public void ResetCapture()
        {
            _FaceInit = true;
            _RightEyeInit = true;
            _LeftEyeInit = true;
        }

        public delegate void EmptyAction();


        public void StartCapture()
        {
            if (!_CaptureInProgress)
            {
                _FaceInit = true;
                _RightEyeInit = true;
                _LeftEyeInit = true;
                if (_wpf)
                {
                    _Timer = new DispatcherTimer();
                    _Timer.Interval = TimeSpan.FromMilliseconds(25);

                    _Timer.Tick += ProcessImage;

                    _Timer.Start();
                }
                else
                {
                    Application.Idle += ProcessImage;
                }
            }

            _CaptureInProgress = true;
        }

        public void StopCapture()
        {
            if (_CaptureInProgress)
            {
                if (_wpf)
                {
                    _Timer.Stop();
                    _GazeCursorWindow.Hide();
                    _GazeCursorWindow.Close();
                }
                else
                {
                    Application.Idle -= ProcessImage;
                    _CameraCursor.Hide();
                }
            }
            _Calibrator.Stop();

            _CaptureInProgress = false;
        }


        private void Initialize()
        {
            if (UseCapture)
            {
                try
                {
                    _Capture = new Capture(_Config.CameraNumber);
                   
                    //_Capture.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FORMAT, 2);
                    //_Capture.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FORMAT.CV_CAP_PROP_FRAME_HEIGHT, 320);
                    //_Capture.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_WIDTH, 240);
                }
                catch (NullReferenceException excpt)
                {
                    MessageBox.Show(excpt.Message);
                }
            }
            else
            {
                _Camera = Camera.GetCamera(_Config.CameraNumber);
                _Camera.CaptureWidth = 320;
                _Camera.CaptureHeight = 240;
            }
        }

        public bool ShowConfig()
        {
            var f = new ConfigForm();
            f.SetConfig(_Config);
            if (f.ShowDialog() == DialogResult.OK)
            {
                _Config = f.GetConfig();
                return true;
            }
            return false;
        }

        #endregion
    }
}